package com.quantum.reflection
{
	import com.quantum.collection.DictionaryMap;
	import com.quantum.collection.DictionarySet;
	import com.quantum.collection.Map;
	import com.quantum.collection.Set;
	import com.quantum.util.StringUtils;

	import flash.utils.Dictionary;
	import flash.utils.describeType;
	import flash.utils.getQualifiedClassName;


	public class ReflectionAccessor
	{



		// The names of the tags that do not need to be included in the reflection info.
		// This is useful to reduce the size of the cached objects.
		// ArrayElementType is needed (see ReflectionUtil.getTaggedArrayElementType).
		private static const IGNORED_TAG_NAMES:Array=[//
			"API",//
			 "Bindable", //
			 "Managed", //
			 "ChangeEvent",//
			  "NonCommittingChangeEvent",//
			   "Transient", //
			   "Inspectable",//
			    "__go_to_definition_help",//
				 "__go_to_ctor_definition_help"//
				 ];

		//		private var _ignoredTagNames:HashSet=new HashSet();

		// The names of commonly used types. This is useful so that all references to these
		// strings use the same string objects.
		private static const COMMON_TYPE_NAMES:Array=[//
			"Object", //
			"Class",//
			 "String",//
			 "Boolean",//
			 "Number",//
			 "Array",//
			 "Date",//
			  "XML",//
			   "int",//
			   "uint", //
			   "void",//
			  "*",//
			   "mx.collections::ArrayCollection"//
			   ];


		private static var _instance:ReflectionAccessor;

		// key and value: common type names, and base classes and interface names.
		// This is useful so that the baseClassNames and interfaceNames properties of
		// ClassObject share the same string objects.
		// Based on ProfilingManager.getObjectSize, this takes about 20k for 250 classes, which
		// was the number I reached after visiting most of the functionalities in
		// vsphere-client MN.
		private var _ignoredMetadataNames:Set=new DictionarySet();


		// key: Class name, value:Dictionary (propertyName --> FieldObject).		
		// It contains the FieldObject objects of the Flex framework classes.
		// This is useful to reduce the size (tremendously) ClassObject.propertyInfos,
		// which includes only the uninherited properties.
		// Based on ProfilingManager.getObjectSize, this takes about 100k for 70 classes, which
		// was the number I reached after visiting most of the functionalities in
		// vsphere-client MN.
		private var _flexFieldObjectsByClassName:Dictionary=new Dictionary();

		// key: Class or class name String, value: ReflectionInfo
		// Based on ProfilingManager.getObjectSize, the average size per 500 cached objects
		// is 1M. So this cache takes about 4M.
		// It is unlikely for this cache to get filled, the number I reached after visiting
		// most of the functionalities in vsphere-client MN was 700.
		private var _classObjectByType:Map=new DictionaryMap();

		private var _sharedTypeNames:Dictionary=new Dictionary();

		public function ReflectionAccessor(token:Class)
		{
			if (token != Singleton)
			{
				throw new Error("ReflectionAccess cannot be instantiated directly.");
			}
			_ignoredMetadataNames.addCollection(IGNORED_TAG_NAMES);
			for each (var typeName:String in COMMON_TYPE_NAMES)
			{
				_sharedTypeNames[typeName]=typeName;
			}
		}

		public static function get instance():ReflectionAccessor
		{
			if (_instance == null)
			{
				_instance=new ReflectionAccessor(Singleton);
			}
			return _instance;
		}


		public function reflect(object:Object):ClassObject
		{
			var cacheKey:Object=getCacheKey(object);
			var classObject:ClassObject=_classObjectByType.getObject(cacheKey) as ClassObject;
			if (classObject == null)
			{
				classObject=newClassObject(object);
				_classObjectByType.put(cacheKey, classObject);
				/* if ((_reflectionInfoByType.count % 20) == 0) {
				   traceCacheSize();
				 } */
			}
			return classObject;
		}

		private function getCacheKey(object:Object):Object
		{
			if (object is Class)
			{
				return object as Class;
			}
			var cacheKey:Object;
			try
			{
				cacheKey=object.constructor;
			}
			catch (error:Error)
			{
				// Some classes that extend flash.utils.Proxy do not handle properly the
				// properties defined in Object.
				// For example see ListCollectionView.getProperty, which throws an exception
				// when called with "constructor" as its argument because it supports
				// only int arguments.
			}
			if (cacheKey == null)
			{
				cacheKey=getQualifiedClassName(object);
			}
			return cacheKey;
		}


		private function newClassObject(object:Object):ClassObject
		{
			var typeXml:XML=newTypeDescription(object);
			var classObject:ClassObject=newClassTypeObject(typeXml);
			classObject.fieldObjects=newFieldObjects(typeXml);
			classObject.methodObjects=newMethodObjects(typeXml);
			return classObject;
		}

		private function newClassTypeObject(typeXml:XML):ClassObject
		{
			var classXml:XML;
			if (typeXml.@isStatic == "true")
			{
				// The passed-in object is a Class, so need to look inside the type
				// description's 'factory' section.
				var xmlList:XMLList=typeXml.factory;
				classXml=xmlList[0];
			}
			else
			{
				classXml=typeXml;
			}
			var classObject:ClassObject=new ClassObject();

			classObject.name=classXml.@name;

			classObject.metadataObjects=newMetadataObjects(classXml.metadata);

			classObject.extendClassNames=getTypeNames(classXml.extendsClass.@type);
			classObject.implementInterfaceNames=getTypeNames(classXml.implementsInterface.@type);

			return classObject;
		}


		private function newFieldObjects(typeXml:XML):Vector.<FieldObject>
		{
			var fieldsXml:XMLList=typeXml..accessor + typeXml..variable;
			var fieldObjects:Vector.<FieldObject>=new Vector.<FieldObject>();
			for each (var fieldXml:XML in fieldsXml)
			{
				var fieldObject:FieldObject=newFieldObject(fieldXml);
				if (null == fieldObject)
				{
					continue;
				}
				fieldObjects.push(fieldObject);
			}
			if (0 == fieldObjects.length)
			{
				fieldObjects=null;
			}
			return fieldObjects;
		}


		private function newFieldObject(fieldXml:XML):FieldObject
		{
			var declaringType:String=fieldXml.@declaredBy;
			var isFlexType:Boolean=isFlexTypeName(declaringType) || declaringType == "Class";
			if (!isFlexType)
			{
				return null;
			}

			var propName:String=fieldXml.@name;

			var flexFieldObjects:Dictionary=_flexFieldObjectsByClassName[declaringType];
			// We check if the property exists even if the class is in the cache because
			// overriden properties have the <code>declaredBy</code> reflection attribute
			// set to the overriding class and not the root class that declares the property.
			// For example, when we do the reflection of the main app class, and given that
			// mx.core.Application overrides the UIComponent "id" property, then declaringType
			// will be returned as mx.core.Application and not UIComponent, and the id property
			// will be cached as a property in mx.core.Application.
			// The check is necessary so that when we reflect on a subclass of UIComponent, the
			// id property is added to the UIComponent cached properties.
			if (flexFieldObjects != null && flexFieldObjects[propName] != null)
			{
				return null;
			}

			var fieldObject:FieldObject=new FieldObject();
			fieldObject.typeName=sharedClassName(fieldXml.@type);

			setAccessibleObjectProperty(fieldObject, fieldXml);

			if (fieldXml.name() == "variable")
			{
				fieldObject.access=AccessorAccess.ACCESS_READWRITE;
				fieldObject.isVariable=true;
			}
			else
			{
				fieldObject.access=fieldXml.@access;
				fieldObject.isVariable=false;
			}
			fieldObject.metadataObjects=newMetadataObjects(fieldXml.metadata);

			//			
			if (flexFieldObjects == null)
			{
				flexFieldObjects=new Dictionary();
				declaringType=sharedClassName(declaringType);
				_flexFieldObjectsByClassName[declaringType]=flexFieldObjects;
			}
			flexFieldObjects[fieldObject.name]=fieldObject;

			return fieldObject;
		}


		private function newMethodObjects(typeXml:XML):Vector.<MethodObject>
		{
//			var methodsXml:XMLList=typeXml..method.(hasOwnProperty("metadata"));
			var methodsXml:XMLList=typeXml..method;
			var methodObjects:Vector.<MethodObject>=new Vector.<MethodObject>();
			for each (var methodXml:XML in methodsXml)
			{
				var methodObject:MethodObject=newMethodObject(methodXml);
				if (null == methodObject)
				{
					continue;
				}
				methodObjects.push(methodObject);
			}
			if (0 == methodObjects.length)
			{
				methodObjects=null;
			}
			return methodObjects;
		}

		private function newMethodObject(methodXml:XML):MethodObject
		{
			var methodObject:MethodObject=new MethodObject();
//			methodObject.returnType=sharedClassName(methodXml.@returnType);

			setAccessibleObjectProperty(methodObject, methodXml);

			//compute method arg type names.
			var parameterXmlList:XMLList=methodXml.parameter;
			if (parameterXmlList.length() != 0)
			{
				var parameterTypeNames:Vector.<String>=new Vector.<String>();
				for each (var param:XML in parameterXmlList)
				{
					var typeName:String=sharedClassName(param.@type);
					parameterTypeNames.push(typeName);
				}
				methodObject.parameterTypeNames=parameterTypeNames;
			}
			if (methodXml.hasOwnProperty("metadata"))
			{
				methodObject.metadataObjects=newMetadataObjects(methodXml.metadata);
			}
			return methodObject;
		}

		private function setAccessibleObjectProperty(accessibleObject:AccessibleObject, accessibleObjectXml:XML):void
		{
			accessibleObject.name=accessibleObjectXml.@name;
			accessibleObject.declaringTypeName=sharedClassName(accessibleObjectXml.@declaredBy);
		}

		private function newMetadataObjects(metadataXmlList:XMLList):Vector.<MetadataObject>
		{
			if (metadataXmlList.length() == 0)
			{
				return null;
			}
			var result:Vector.<MetadataObject>=new Vector.<MetadataObject>();
			for each (var metadataXml:XML in metadataXmlList)
			{
				var metadataName:String=metadataXml.@name;
				if (_ignoredMetadataNames.contains(metadataName))
				{
					continue;
				}
				var metadataObject:MetadataObject=new MetadataObject();
				metadataObject.name=metadataName;
				for each (var argXml:XML in metadataXml.arg)
				{
					var metadataArgument:MetadataArgument=new MetadataArgument();
					metadataArgument.key=StringUtils.nullIfEmpty(argXml.@key);
					metadataArgument.value=argXml.@value;
					metadataObject.addMetadataArgument(metadataArgument);
				}

				result.push(metadataObject);
			}
			if (result.length == 0)
			{
				result=null;
			}
			return result;
		}

		private function getTypeNames(xmlList:XMLList):Vector.<String>
		{
			if (xmlList.length() == 0)
			{
				return null;
			}
			var typeNames:Vector.<String>=new Vector.<String>();
			for each (var nameXml:XML in xmlList)
			{
				var name:String=String(nameXml);
				name=sharedClassName(name, true);
				typeNames.push(name);
			}
			if (typeNames.length == 0)
			{
				typeNames=null;
			}
			return typeNames;
		}

		private function sharedClassName(name:String, cache:Boolean=false):String
		{
			if (StringUtils.isEmpty(name))
			{
				return null;
			}
			var result:String=_sharedTypeNames[name];
			if (result == null)
			{
				result=name;
				if (cache)
				{
					_sharedTypeNames[name]=name;
				}
			}
			return result;
		}


		private function newTypeDescription(object:Object):XML
		{
			var typeXml:XML=describeType(object);
			return typeXml;
		}

		/**
		 * Indicates whether the given type name represents a flex type. For example, the
		 * types in the mx and flash namespaces, the resource asset types, or the root Object
		 * type. Notably, class names including "__" are considered to be Flex assets types.
		 * This method performs a conservative check, i.e., a return value of false does not
		 * necessarily mean that the type is not a flex framework type, but a return value of
		 * true means that it definitely is.
		 */
		public static function isFlexTypeName(typeName:String):Boolean
		{
			// "__" filters out types like:
			// en_US$FlexWidgets_properties__embed_properties_______assets_swfs_...
			// and _ContainerApp_Styles__embed_css_assets_images_arrowRight_png_1338931880.

			// "mx_managers_SystemManager" filters out types like
			// _ContainerApp_mx_managers_SystemManager.
			if (typeName.indexOf("mx.") == 0 || typeName.indexOf("spark.") == 0 || typeName.indexOf("flash.") == 0 || typeName == "Object" || typeName.indexOf("__") >= 0 || typeName.indexOf("mx_managers_SystemManager") >= 0)
			{
				return true;
			}
			return false;
		}
	}
}

class Singleton
{
}